/*
 * Copyright 2009, Team Mondegreen:
 * Brian Hsieh - bh2181@columbia.edu
 * Eugene Yao - eyy2102@columbia.edu
 * Jake Amonette - jaa2149@columbia.edu
 * Tal Giat - tg2235@colubia.edu
 * 
 * This file is part of AMFM (A Minimal Fractal Music Language), a language created for
 * fractal music as part of the Programming Languages and Translator class at Columbia university.
 *
 * For more details go to: http://code.google.com/p/amfm/
 * 
 * AMFM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * at your option any later version.
 * 
 * AMFM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with AMFM.  If not, see <http://www.gnu.org/licenses/>. 
 */

package com.myronmarston.music;

import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;

import com.myronmarston.util.Fraction;
import com.myronmarston.util.MathHelper;

/**
 * 
 * Manages output for FractalComposer. Modified to allow a simpler interface with AMFM that can still use the
 * FractalComposer background capability.
 * 
 * @author Jake
 */
public class AMFMOutputManager {

  private NoteList[] noteListArray;
  private Sequence sequence;
  private AMFMAudioFileCreator audioFileCreator;
  private String lastWavFileName;

  /**
   * Converts the midi tick unit from quarter notes to whole notes, using ints.
   * 
   * @param ticksInWholeNotes ticks in whole notes
   * @return ticks in quarter notes
   */
  private static int convertMidiTickUnitFromQuarterNotesToWholeNotesInt(int ticksInWholeNotes) {
    return ticksInWholeNotes * 4;
  }

  /**
   * 
   * @param noteLists
   * @throws GermIsEmptyException
   */
  public AMFMOutputManager(NoteList[] noteLists) throws GermIsEmptyException {
    this.noteListArray = noteLists;
    constructMidiSequence();
  }

  /**
   * Gets the Midi sequence.
   * 
   * @return the midi sequence
   */
  public Sequence getSequence() {
    return sequence;
  }

  /**
   * Gets the audio file creator.
   * 
   * @return the audio file creator
   */
  public AMFMAudioFileCreator getAudioFileCreator() {
    if (audioFileCreator == null)
      audioFileCreator = new AMFMAudioFileCreator(this);
    return audioFileCreator;
  }

  /**
   * Gets the file name of the last wav file saved using this output manager.
   * 
   * @return the last wav file
   */
  public String getLastWavFileName() {
    return lastWavFileName;
  }

  /**
   * Saves the music as a wav file.
   * 
   * @param fileName the file name to save to
   * @throws javax.sound.midi.MidiUnavailableException if there is a midi problem
   * @throws java.io.IOException if there is an i/o error
   */
  public void saveWavFile(String fileName) throws MidiUnavailableException, IOException {
    this.getAudioFileCreator().saveWavFile(fileName);
    this.lastWavFileName = fileName;
  }

  /**
   * Constructs a midi track based on the given note list.
   * 
   * @param noteList the note list
   */
  protected void constructMidiTrack(NoteList noteList) {
    MidiNote thisMidiNote, lastMidiNote = null;
    Note lastNote = null;
    Fraction startTime = new Fraction(0, 1);

    // get a default instrument if we we're not passed one...
    Instrument instrument;
    if (noteList.getInstrument() == null) {
      instrument = Instrument.DEFAULT;
    } else {
      instrument = noteList.getInstrument();
    }

    // make each track be on a different channel, but make sure we don't go over our total number of
    // channels...
    int numTracks = sequence.getTracks().length;
    // The 1st track should be the tempo/key sig/time sig track
    // assert numTracks > 0 : numTracks;
    int midiChannel = numTracks;
    assert midiChannel < MidiNote.MAX_CHANNEL;

    Track track = sequence.createTrack();
    track.add(instrument.getProgramChangeMidiEvent(midiChannel));

    // in Midi, the tick resolution is based on quarter notes, but we use whole notes...
    int midiTicksPerWholeNote = convertMidiTickUnitFromQuarterNotesToWholeNotesInt(sequence.getResolution());

    for (Note thisNote : noteList.getListWithNormalizedRests()) {

      thisMidiNote = thisNote.convertToMidiNote(startTime, midiTicksPerWholeNote, midiChannel, true);

      if (lastMidiNote != null) {

        assert lastNote != null;

        if (thisMidiNote.getPitch() == lastMidiNote.getPitch()
            && lastNote.getNormalizedNote().getScaleStep() != thisNote.getNormalizedNote().getScaleStep()) {
          // the notes are different scale steps and should have different pitches.
          // This can happen with notes like B# and C in the key of C.

          if (lastNote.getChromaticAdjustment() != 0) {
            lastMidiNote = lastNote.convertToMidiNote(startTime.minus(thisNote.getDuration()),
                midiTicksPerWholeNote, midiChannel, false);
          } else if (thisNote.getChromaticAdjustment() != 0) {
            thisMidiNote = thisNote.convertToMidiNote(startTime, midiTicksPerWholeNote, midiChannel, false);
          } else {
            // one of these notes should always have a chromatic
            // adjustment--otherwise, how do they have the same pitches
            // but different scale steps?
            assert false : "Neither last note '" + lastNote.toString() + "' nor this note '"
                + thisNote.toString() + "' have a chromatic adjustment.";
          }

          assert thisMidiNote.getPitch() != lastMidiNote.getPitch() : "The midi notes have the same pitch and should not: "
              + thisMidiNote.getPitch();
        }

        try {
          track.add(lastMidiNote.getNoteOnEvent());
          track.add(lastMidiNote.getNoteOffEvent());
        } catch (InvalidMidiDataException ex) {
          // our logic should prevent this exception from ever occurring,
          // so we transform this to an unchecked exception instead of
          // having to declare it on our method.
          throw new UndeclaredThrowableException(
              ex,
              "MidiNote's note on and note off events could not be created.  This indicates a programming error of some sort.");
        }
      }

      // The next note start time will be the end of this note...
      startTime = startTime.plus(thisNote.getDuration());

      lastMidiNote = thisMidiNote;
      lastNote = thisNote;
    }

    try {
      track.add(lastMidiNote.getNoteOnEvent());
      track.add(lastMidiNote.getNoteOffEvent());
    } catch (InvalidMidiDataException ex) {
      // our logic should prevent this exception from ever occurring,
      // so we transform this to an unchecked exception instead of
      // having to declare it on our method.
      throw new UndeclaredThrowableException(
          ex,
          "MidiNote's note on and note off events could not be created.  This indicates a programming error of some sort.");
    }

  }

  /**
   * Calculates the optimal midi tick resolution for the given collection of noteLists, based on the duration
   * of the notes.
   * 
   * @return the midi tick resolution
   */
  protected int getMidiTickResolution() {
    // next, figure out the resolution of our Midi sequence...
    ArrayList<Long> uniqueDurationDenominators = new ArrayList<Long>();

    for (NoteList nl : this.noteListArray) {
      for (Note n : nl) {
        if (!uniqueDurationDenominators.contains(n.getDuration().denominator())) {
          uniqueDurationDenominators.add(n.getDuration().denominator());
        }
      }
    }

    long resolution = MathHelper.leastCommonMultiple(uniqueDurationDenominators);
    assert resolution < Integer.MAX_VALUE;
    return (int) resolution;
  }

  /**
   * Creates the midi sequence and the guido notation
   * 
   * @throws com.myronmarston.music.GermIsEmptyException if the germ of one of the fractal pieces given is
   *           empty
   */
  private void constructMidiSequence() throws GermIsEmptyException {

    // this is only meant to be called once, to construct the sequence...
    assert this.sequence == null : sequence;

    try {
      this.sequence = new Sequence(Sequence.PPQ, this.getMidiTickResolution());
    } catch (InvalidMidiDataException ex) {
      // our logic should prevent this exception from ever occurring,
      // so we transform this to an unchecked exception instead of
      // having to declare it on our method.
      throw new UndeclaredThrowableException(ex,
          "Error while creating sequence.  This indicates a programming error of some sort.");
    }

    for (NoteList nl : this.noteListArray) {
      constructMidiTrack(nl);
    }

  }
}